🎖️GitЯра🎖️
Commit 1b661739e346f6d64ff06731ee2ac0bc80501d19
Parents : 813acee
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-06-01T17:43:22-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-06-01T22:43:22Z
fix(map): scope cluster-renderer ViewTreeLifecycleOwner to map host view (#5708)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Changes
1 files changed, 45 insertions(+), 0 deletions(-)
Diff
diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
index 567e146cc6..6d38e176af 100644
--- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
@@ -17,7 +17,18 @@
package org.meshtastic.app.map.component
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.compose.currentStateAsState
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.savedstate.compose.LocalSavedStateRegistryOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.maps.android.clustering.Cluster
import com.google.maps.android.clustering.view.DefaultClusterRenderer
import com.google.maps.android.compose.Circle
@@ -36,6 +47,40 @@ fun NodeClusterMarkers(
navigateToNodeDetails: (Int) -> Unit,
onClusterClick: (Cluster<NodeClusterItem>) -> Boolean,
) {
+ val view = LocalView.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current
+ val lifecycleState by lifecycleOwner.lifecycle.currentStateAsState()
+
+ // maps-compose renders each non-clustered item to a bitmap through an off-screen ComposeView that
+ // it attaches under the MapView (see ComposeUiClusterRenderer + NoDrawContainerView in
+ // MapComposeViewRender). That ComposeView walks up the view tree for a ViewTreeLifecycleOwner and,
+ // when it finds none, crashes with "Composed into the View which doesn't propagate
+ // ViewTreeLifecycleOwner!" (googlemaps/android-maps-compose#875 / #325) — a FATAL on the map screen.
+ //
+ // Propagate the owners onto this map screen's host view (LocalView.current), which is an ancestor
+ // of the internally-created MapView, so the renderer's ComposeView can resolve them. We deliberately
+ // do NOT touch view.rootView (the activity root): attaching a transient NavEntry lifecycle there is
+ // what caused the node-list popup regression (#5684), which is why #5704 removed the prior, broader
+ // workaround entirely. Scoping to the map host view and restoring the previous owners on dispose
+ // keeps the fix local to the map and leaves Popups/DropdownMenus untouched.
+ DisposableEffect(view, lifecycleOwner, savedStateRegistryOwner) {
+ val prevLifecycleOwner = view.findViewTreeLifecycleOwner()
+ val prevSavedStateRegistryOwner = view.findViewTreeSavedStateRegistryOwner()
+ view.setViewTreeLifecycleOwner(lifecycleOwner)
+ view.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
+ onDispose {
+ view.setViewTreeLifecycleOwner(prevLifecycleOwner)
+ view.setViewTreeSavedStateRegistryOwner(prevSavedStateRegistryOwner)
+ }
+ }
+
+ // The cluster renderer drives marker rendering from an async Handler (DefaultClusterRenderer's
+ // MarkerModifier), which can fire after this screen has stopped and the internal ComposeView is
+ // detached — at which point no owner is reachable regardless of the above. Skip rendering once the
+ // lifecycle is no longer at least STARTED to close most of that race.
+ if (!lifecycleState.isAtLeast(Lifecycle.State.STARTED)) return
+
Clustering(
items = nodeClusterItems,
onClusterClick = onClusterClick,
Served by rngit 1.5.0 - Generated in 0.04s